// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! Utilities for parsing and generating Cargo.toml and related manifest files.

use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Set of dependencies for a particular usage: final artifacts, tests, or
/// build scripts.
pub type DependencySet = BTreeMap<String, Dependency>;
/// Set of patches to replace upstream dependencies with local crates. Maps
/// arbitrary patch names to `CargoPatch` which includes the actual package name
/// and the local path.
pub type CargoPatchSet = BTreeMap<String, CargoPatch>;

/// A specific crate version.
pub use semver::Version;

/// A version constraint in a dependency spec. We don't use `semver::VersionReq`
/// since we only pass it through opaquely from third_party.toml to Cargo.toml.
/// Parsing it is unnecessary.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
// From serde's perspective we serialize and deserialize this as a plain string.
#[serde(transparent)]
pub struct VersionConstraint(pub String);

/// Parsed third_party.toml. This is a limited variant of Cargo.toml.
#[derive(Clone, Debug, Deserialize)]
pub struct ThirdPartyManifest {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<WorkspaceSpec>,
    #[serde(flatten)]
    pub dependency_spec: DependencySpec,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct WorkspaceSpec {
    pub members: Vec<String>,
}

/// The sets of all types of dependencies for a manifest: regular, build script,
/// and test. This should be included in other structs with `#[serde(flatten)]`
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DependencySpec {
    /// Regular dependencies built into production code.
    #[serde(
        default,
        skip_serializing_if = "DependencySet::is_empty",
        serialize_with = "toml::ser::tables_last"
    )]
    pub dependencies: DependencySet,
    /// Test-only dependencies.
    #[serde(
        default,
        skip_serializing_if = "DependencySet::is_empty",
        serialize_with = "toml::ser::tables_last"
    )]
    pub dev_dependencies: DependencySet,
    /// Build script dependencies.
    #[serde(
        default,
        skip_serializing_if = "DependencySet::is_empty",
        serialize_with = "toml::ser::tables_last"
    )]
    pub build_dependencies: DependencySet,
}

/// A single crate dependency.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Dependency {
    /// A dependency of the form `foo = "1.0.11"`: just the package name as key
    /// and the version as value. The sole field is the crate version.
    Short(VersionConstraint),
    /// A dependency that specifies other fields in the form of `foo = { ... }`
    /// or `[dependencies.foo] ... `.
    Full(FullDependency),
}

/// A single crate dependency with some extra fields from third_party.toml.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct FullDependency {
    /// Version constraint on dependency.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<VersionConstraint>,
    /// Required features.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<String>,
    /// Whether this can be used directly from Chromium code, or only from other
    /// third-party crates.
    #[serde(default = "get_true", skip_serializing_if = "is_true")]
    pub allow_first_party_usage: bool,
    /// List of files generated by build.rs script.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub build_script_outputs: Vec<String>,
}

/// Representation of a Cargo.toml file.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoManifest {
    pub package: CargoPackage,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<WorkspaceSpec>,
    #[serde(flatten)]
    pub dependency_spec: DependencySpec,
    #[serde(default, rename = "patch")]
    pub patches: BTreeMap<String, CargoPatchSet>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoPackage {
    pub name: String,
    pub version: Version,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub authors: Vec<String>,
    #[serde(default)]
    pub edition: Edition,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub license: String,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct Edition(pub String);

impl Default for Edition {
    fn default() -> Self {
        Edition("2015".to_string())
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoPatch {
    pub path: String,
    pub package: String,
}

// Used to set the serde default of a field to true.
fn get_true() -> bool {
    true
}

fn is_true(b: &bool) -> bool {
    *b
}

#[derive(Debug)]
pub struct PatchSpecification {
    pub package_name: String,
    pub patch_name: String,
    pub path: PathBuf,
}

pub fn generate_fake_cargo_toml<Iter: IntoIterator<Item = PatchSpecification>>(
    third_party_manifest: ThirdPartyManifest,
    patches: Iter,
) -> CargoManifest {
    let ThirdPartyManifest { workspace, mut dependency_spec, .. } = third_party_manifest;

    // Hack: set all `allow_first_party_usage` fields to true so they are
    // suppressed in the Cargo.toml.
    for dep in [
        dependency_spec.dependencies.values_mut(),
        dependency_spec.build_dependencies.values_mut(),
        dependency_spec.dev_dependencies.values_mut(),
    ]
    .into_iter()
    .flatten()
    {
        if let Dependency::Full(ref mut dep) = dep {
            dep.allow_first_party_usage = true;
        }
    }

    let mut patch_sections = CargoPatchSet::new();
    // Generate patch section.
    for PatchSpecification { package_name, patch_name, path } in patches {
        patch_sections.insert(
            patch_name,
            CargoPatch { path: path.to_str().unwrap().to_string(), package: package_name },
        );
    }

    let package = CargoPackage {
        name: "chromium".to_string(),
        version: Version::new(0, 1, 0),
        authors: Vec::new(),
        edition: Edition("2021".to_string()),
        description: None,
        license: "".to_string(),
    };

    CargoManifest {
        package,
        workspace,
        dependency_spec,
        patches: std::iter::once(("crates-io".to_string(), patch_sections)).collect(),
    }
}
